package org.jnarcissus;

import java.lang.reflect.Method;

import net.sf.cglib.core.ClassGenerator;
import net.sf.cglib.core.DefaultGeneratorStrategy;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import net.sf.cglib.transform.TransformingClassGenerator;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.StringDescription;
import org.jbrisk.exceptions.NullArgumentException;
import org.jbrisk.tests.ExceptionAssert;
import org.jbrisk.tests.IExceptionAssertRunner;
import org.jbrisk.text.StringUtils;

public class JNarcissusCore implements MethodInterceptor {

	public static ThreadLocal<Boolean> isCreatingMethodCall = new ThreadLocal<Boolean>() {

		protected Boolean initialValue() {

			return false;
		};
	};

	public static ThreadLocal<Boolean> isIntercepting = new ThreadLocal<Boolean>() {

		protected Boolean initialValue() {

			return false;
		};
	};

	private static ThreadLocal<MonitoredInstanceMethodMatcher> currentMethodMatcher = new ThreadLocal<MonitoredInstanceMethodMatcher>();

	private static ThreadLocal<MonitoredInstanceMethodCall> currentMethod = new ThreadLocal<MonitoredInstanceMethodCall>();

	public void assertInstance(IMonitoredInstance instanceToAssert) {

		if (instanceToAssert == null)
			throw new NullArgumentException("instanceToAssert");

		instanceToAssert.getMonitoredData().andPreviousAsserts();
	}

	public void assertInstance(Object instanceToAssert) {

		if (!(instanceToAssert instanceof IMonitoredInstance))
			throw new IllegalArgumentException("The supplied instance does not implement the \"" + IMonitoredInstance.class.getName()
					+ "\" interface, therefore it cannot be asserted!");

		assertInstance((IMonitoredInstance) instanceToAssert);
	}

	public <T> T assertMethod(T monitoredInstance, MonitoredInstanceMethodMatcher matcher) {

		if (monitoredInstance == null)
			throw new NullArgumentException("monitoredInstance");

		if (matcher == null)
			throw new NullArgumentException("matcher");

		if (currentMethodMatcher.get() != null)
			throw new JNarcissusException("There is an assertion being made! Please conclude it by calling a method from the monitored instance.");

		currentMethodMatcher.set(matcher);

		return monitoredInstance;
	}

	public <T> MonitoredInstanceData assertThat(String reason, T methodCall, Matcher<T> matcher) throws AssertionError {

		MonitoredInstanceMethodCall current = (MonitoredInstanceMethodCall) currentMethod.get();

		if (matcher == null)
			throw new NullArgumentException("matcher");

		if (current == null)
			throw createNoCurrentInvocationException();

		MonitoredInstanceMethodCallAssert methodCallAssert = new MonitoredInstanceMethodCallAssert(current);
		methodCallAssert.setMatcher(matcher);
		methodCallAssert.setReason(reason);

		current.setMethodCallAssert(methodCallAssert);

		currentMethod.set(null);

		methodCallAssert.execute(false);

		return current.getSource().getMonitoredData();
	}

	public <T> MonitoredInstanceData assertThat(T methodCall, Matcher<T> matcher) throws AssertionError {

		return assertThat(null, methodCall, matcher);
	}

	public <T extends Throwable> MonitoredInstanceData assertThatThrows(Class<T> expectedExceptionClass, IExceptionAssertRunner runner) {

		return assertThatThrows(null, expectedExceptionClass, runner);
	}

	public <T extends Throwable> MonitoredInstanceData assertThatThrows(String expectedExceptionMessage, Class<T> expectedExceptionClass,
			IExceptionAssertRunner runner) {

		if (expectedExceptionClass == null)
			throw new NullArgumentException("expectedExceptionClass");

		if (runner == null)
			throw new NullArgumentException("runner");

		// First, executes the assert, this way it will intercept the calling
		// method.
		ExceptionAssert.assertException(expectedExceptionMessage, expectedExceptionClass, runner);

		MonitoredInstanceMethodCall current = currentMethod.get();

		if (current == null)
			throw createNoCurrentInvocationException();

		MonitoredInstanceMethodCallExceptionAssert<T> methodCallAssert = new MonitoredInstanceMethodCallExceptionAssert<T>(current);
		methodCallAssert.setExpectedExceptionMessage(expectedExceptionMessage);
		methodCallAssert.setExpectedExceptionClass(expectedExceptionClass);
		methodCallAssert.setRunner(runner);

		current.setMethodCallAssert(methodCallAssert);

		currentMethod.set(null);

		return current.getSource().getMonitoredData();
	}

	/**
	 * Creates an instance from the supplied class with the supplied constructor
	 * arguments.
	 * 
	 * @param <T>
	 *            Class type.
	 * @param classToCreateFrom
	 *            The class to create the instance from.
	 * @param constructorArgsTypes
	 *            List of ordered argument types. It will use this value to find
	 *            the proper constructor.
	 * @param constructorArgs
	 *            List of arguments to supply to the class constructor.
	 * @return Created instance.
	 */
	public <T> T create(Class<T> classToCreateFrom, Class<?>[] constructorArgsTypes, Object[] constructorArgs) {

		return createImplementation(classToCreateFrom, constructorArgsTypes, constructorArgs);
	}

	/**
	 * Implementation of the create method.
	 * 
	 * @param <T>
	 *            Class type.
	 * @param classToCreateFrom
	 *            The class to create the instance from.
	 * @param constructorArgsTypes
	 *            List of ordered argument types. It will use this value to find
	 *            the proper constructor.
	 * @param constructorArgs
	 *            List of arguments to supply to the class constructor.
	 * @return Created instance.
	 */
	@SuppressWarnings("unchecked")
	private <T> T createImplementation(Class<T> classToCreateFrom, Class<?>[] constructorArgsTypes, Object[] constructorArgs) {

		if (classToCreateFrom == null)
			throw new NullArgumentException("classToCreateFrom");

		Enhancer enhancer = new Enhancer();
		enhancer.setSuperclass(classToCreateFrom);
		enhancer.setInterfaces(classToCreateFrom.getInterfaces());
		
		// enhancer.setCallbackFilter(new CallbackFilter() {
		//
		// @Override
		// public int accept(Method arg0) {
		//
		// int accept = 0;
		//
		// if (arg0.getReturnType().equals(Void.TYPE)) {
		//
		// accept = 1;
		// }
		//
		// return accept;
		// }
		// });

		enhancer.setCallbacks(new Callback[] { this });

		enhancer.setStrategy(new DefaultGeneratorStrategy() {

			protected ClassGenerator transform(ClassGenerator cg) {

				return new TransformingClassGenerator(cg, new JNarcissusClassEmitterTransformer());
			}
		});

		T monitoredInstance = null;

		if (constructorArgs != null && constructorArgs.length > 0) {

			if (constructorArgsTypes == null) {

				constructorArgsTypes = new Class<?>[constructorArgs.length];

				for (int i = 0; i < constructorArgs.length; i++) {

					constructorArgsTypes[i] = constructorArgs[i].getClass();
				}
			}

			monitoredInstance = (T) enhancer.create(constructorArgsTypes, constructorArgs);

		} else {

			monitoredInstance = (T) enhancer.create();
		}

		return monitoredInstance;
	}

	protected JNarcissusException createNoCurrentInvocationException() {
		return new JNarcissusException(
				"There is no current monitored instance invocation! Please certify that the \"methodCall\" argument contains a call to a non-final method of a monitored class!");
	}

	/**
	 * Returns the current thread {@link MonitoredInstanceMethodCall} instance.
	 * Every time a monitored method gets called, this value will change.
	 * 
	 * @return Current thread {@link MonitoredInstanceMethodCall} instance.
	 */
	public MonitoredInstanceMethodCall getCurrentMonitoredMethod() {

		return currentMethod.get();
	}

	public Object intercept(Object source, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

		Object returnValue = null;

		// This verification is made because if on of the arguments is a MonitoredInstance, then its equals
		// method can be called, therefore result in a StackOverflow
		if (!isCreatingMethodCall.get()) {
			
			IMonitoredInstance monitoredInstance = (IMonitoredInstance) source;

			isCreatingMethodCall.set(true);
			
			MonitoredInstanceMethodCall monitoredMethod = monitoredInstance.getMonitoredData().getMethod(monitoredInstance, method, args, methodProxy);

			isCreatingMethodCall.set(false);
			
			if (!isIntercepting.get()) {

				try {

					// Use this semaphore to prevent a monitored method that
					// call
					// another monitored method to overwrite the current
					// invocation.
					isIntercepting.set(true);

					MonitoredInstanceMethodMatcher monitoredMethodMatcher = currentMethodMatcher.get();

					if (monitoredMethodMatcher == null) {

						// If its not asserting the intercepted method, call the
						// real implementation
						currentMethod.set(monitoredMethod);

						monitoredMethod.incrementCallCount();
						returnValue = methodProxy.invokeSuper(source, args);
						monitoredMethod.setLastCallReturnValue(returnValue);

					} else {

						// If is asserting the intercepted method, executes the
						// matcher and cleanup
						currentMethodMatcher.set(null);

						if (!monitoredMethodMatcher.matchesSafely(monitoredMethod)) {
							Description description = new StringDescription();

							description.appendText("Asserting method: ");
							monitoredMethod.appendMethodDescription(description);
							description.appendText(StringUtils.NEW_LINE);
							description.appendDescriptionOf(monitoredMethodMatcher);

							throw new java.lang.AssertionError(description.toString());
						}
					}

				} finally {

					isIntercepting.set(false);
				}

			} else {

				monitoredMethod.incrementCallCount();
				returnValue = methodProxy.invokeSuper(source, args);
			}
		}

		return returnValue;
	}

	public <T> void removeAssert(T methodCall) throws AssertionError {

		MonitoredInstanceMethodCall current = currentMethod.get();

		if (current == null)
			throw createNoCurrentInvocationException();

		current.setMethodCallAssert(null);

		currentMethod.set(null);
	}

	/**
	 * Resets the current values. Use this to remove any unused monitored
	 * invocation, etc.
	 */
	public void reset() {

		currentMethod.set(null);
	}
}
